package rdbms.ddl;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;

import rdbms.Entry;
import rdbms.Group;
import rdbms.RDBMS;
import valuetypes.CharacterValue;
import valuetypes.DBValue;
import valuetypes.GroupValue;
import valuetypes.NumericValue;
import valuetypes.ValueType;
import exception.DBValueException;
import exception.EntryException;
import exception.IllegalNameException;
import exception.InvalidFieldException;
import exception.NoSuchDatabaseException;


public class Table  {

	private static final String FIELD_ID = "_id";
	private static final String GROUP_PRIMARY_KEY = "pKey";
	private static int fieldIdGen = 0;
	private static int groupIdGen = 0;
	
	private Database parent;
	private CharacterValue name;
	private Group primaryKey;
	private LinkedHashMap<Integer, Field> fields;
	private LinkedHashMap<String, Integer> fieldIndexName;
	private LinkedHashMap<Integer, Group> groups;
	private LinkedHashMap<Integer, Entry> entries;
	private LinkedHashMap<GroupValue, Integer> entryIndexPKey;
	private int entryIdGen = 0;

	private NumericValue id;
	
	
	
//	public Table(Database database, String tableName) throws IllegalNameException  {
//		try {
//			this.name = new CharacterValue(tableName);
//		} catch (DBValueException e) {
//			throw IllegalNameException.fromDBValueException(e);
//		}		
//		this.parent = database;		
//		fields = new LinkedHashMap<String, Field>();
//		entries = new ArrayList<Entry>();
////		fieldIndices = new HashMap<Integer, Field>();
//	}
	
	public Table(byte[] data, RDBMS rdbms) throws DBValueException, NoSuchDatabaseException{
		groups = new LinkedHashMap<>();
		fields = new LinkedHashMap<>();
		fieldIndexName = new LinkedHashMap<>();
		entries = new LinkedHashMap<>();
		entryIndexPKey = new LinkedHashMap<>();
		int offset = 0;
		this.id = new NumericValue(data, offset);
		offset += id.getSize(); 
		
		NumericValue dbId = new NumericValue(data, offset);
		this.parent = rdbms.getDatabase(dbId);
		offset += dbId.getSize();
		
		this.name = new CharacterValue(data, offset);
		offset += name.getSize();
		
		parent.addTable(this);
	}
	
	public Table(Database database, NumericValue tableId, String tableName, List<Field> fieldList, Field[] primaryKeyFields) throws IllegalNameException, DBValueException, InvalidFieldException  {
		try {
			this.name = new CharacterValue(tableName);
		} catch (DBValueException e) {
			throw IllegalNameException.fromDBValueException(e);
		}		
		this.parent = database;
		this.id = tableId;
		groups = new LinkedHashMap<>();
		fields = new LinkedHashMap<>();
		fieldIndexName = new LinkedHashMap<>();
		entries = new LinkedHashMap<>();
		entryIndexPKey = new LinkedHashMap<>();
				
		Field idField = new Field(FIELD_ID, ValueType.Numeric, false);		
		addField(idField);
		if(primaryKeyFields == null || primaryKeyFields.length == 0){
			primaryKeyFields = new Field[]{idField};
		}		
		
		
		for(Field f : fieldList){
			addField(f);
		}
		
		
		int gId = ++groupIdGen;
		Group pKey = new Group(gId, GROUP_PRIMARY_KEY, this, primaryKeyFields, true, true); 
		addGroup(pKey);
		

	}
	
	public int insert(List<DBValue> values, List<String> fieldNames) throws InvalidFieldException, EntryException {
		int  id = -1;
		if(values.size() == fieldNames.size()){
			Entry entry = new Entry(this);
			
			for(int i = 0; i < values.size(); i++){
				entry.setValue(fieldNames.get(i), values.get(i));				
			}
			if(entry.validate()){
				int tempId = entryIdGen+1;
				entry.setID(tempId);
				if( validateGroups(entry)){
					id = ++entryIdGen;
					entry.setID(id);
					entries.put(id, entry);					
					entryIndexPKey.put(entry.getGroupValue(primaryKey), id);
					System.out.println("Table: " + getName() + " contains: " + entries.size() + " entries");
				}				
			}		
		}
		return id;
		
	}
	
	private boolean validateGroups(Entry entry) {
		for(Group g : groups.values()){
			if(!g.validate(entry)){
				return false;
			}
		}
		return true;
	}

	public int insert(DBValue[] values, String[] fieldNames) throws InvalidFieldException, EntryException{
		return insert(Arrays.asList(values), Arrays.asList(fieldNames));
	}
	
	@SuppressWarnings("unchecked")
	public HashMap<String, Field> getFields() {
		//Clone to prevent tampering.
		return (HashMap<String, Field>) fields.clone();
	}

	public Field getField(String fieldName) throws InvalidFieldException {
		Field f = fields.get(fieldIndexName.get(fieldName));
		if(f == null)throw InvalidFieldException.NoSuchField(fieldName, this);
		return f;
	}
	
	public Field getField(int fieldId){
		Field f = fields.get(fieldId);
		return f;
	}

	public int getFieldCount() {
		return fields.size();
	}

	public String getName() {
		return name.toString();
	}
	
	public int addField(Field field) throws IllegalNameException, InvalidFieldException{
		if(fieldIndexName.containsKey(field.getName())){
			throw IllegalNameException.fieldExists(getName(), field.getName());
		}
		else{
			int id = ++fieldIdGen;
			fields.put(id, field);
			fieldIndexName.put(field.getName(), id);
			field.setTable(this, id);
		}		
		return getFieldCount();
	}
	
	public Group addGroup(Group group){
		int gId = group.getID().getValue();
		if(gId <= 0 || groups.containsKey(gId))	{
			//TODO errorhandling
		}
		else if (primaryKey != null && group.isPrimaryKey()){
			//TODO errorhandling
		}
		else{
			if(gId > groupIdGen)groupIdGen = gId;
			
			if(group.isPrimaryKey())this.primaryKey = group;
			groups.put(gId, group);
			return group;
		}
		return null;
	}
	
	public Group addGroup( String name, Field[] fields, boolean unique) throws DBValueException, InvalidFieldException{
		return addGroup(new Group(++groupIdGen, name, this, fields, false, unique));
	}

	public List<DBValue> getFieldValues(Field field) {
		List<DBValue> values = new ArrayList<DBValue>();
		for(Entry e : entries.values()){
			values.add(e.getValue(field));
		}
		return values;
	}

	public boolean containsField(Field f) {
		Field mine = fields.get(f.getId().getValue());
//		Field mine = fields.get(f.getName());
		return f.equals(mine);
	}

	public byte[] serialize() {
		return DBValue.valuesAsData(id,  parent.getId(), name);
	}
	
	@Override
	public String toString() {
		return id + "\t| " + name + "\t| " +  parent.getId();
	}

	public Collection<? extends Group> getGroups() {
		return groups.values();
	}

	public Collection<? extends Entry> getEntries() {
		System.out.println("Table: " + getName() + " contains: " + entries.size() + " entries");
		return entries.values();
	}

	public NumericValue getId() {
		return id;
	}

	public Database getDatabase() {
		return parent;
	}

	public Group getGroup(NumericValue groupId) {
		return getGroup(groupId.getValue());
	}
	
	public Group getGroup(int groupId) {
		return groups.get(groupId);
	}

	public Field getField(NumericValue fieldId) {
		return getField(fieldId.getValue());
	}

	public static boolean insertData(byte[] data, RDBMS rdbms, boolean verbose) throws DBValueException, NoSuchDatabaseException, InvalidFieldException, EntryException{
		int offset = 0;
		NumericValue entryId = new NumericValue(data, offset);
		offset += entryId.getSize();
		
		NumericValue dbId = new NumericValue(data, offset);
		offset += dbId.getSize();
		
		NumericValue tableId = new NumericValue(data, offset);
		offset += tableId.getSize();
		
//		NumericValue gId = new NumericValue(data, offset);
//		offset += gId.getSize();
//		
//		NumericValue fieldId = new NumericValue(data, offset);
//		offset += fieldId.getSize();
		
		Table table = rdbms.getDatabase(dbId).getTable(tableId);		
		Collection<Field> fields = table.getFields().values();
		
		ArrayList<String> fieldNames = new ArrayList<>();
		ArrayList<DBValue> values = new ArrayList<>();
		for(Field f : fields){			
			Class<? extends DBValue> type = f.getFieldType().getBaseClass();
			Constructor<? extends DBValue> constructor;
			try {
				constructor = type.getConstructor(byte[].class, int.class);
				DBValue value = constructor.newInstance(data, offset);
				offset += value.getSize();
				
				values.add(value);
				fieldNames.add(f.getName());
			} catch (NoSuchMethodException e) {
				e.printStackTrace();
				return false;
			} catch (SecurityException e) {
				e.printStackTrace();
				return false;
			} catch (InstantiationException e) {
				e.printStackTrace();
				return false;
			} catch (IllegalAccessException e) {
				e.printStackTrace();
				return false;
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
				return false;
			} catch (InvocationTargetException e) {
				e.printStackTrace();
				return false;
			}
			
		}
	
		table.insert(values, fieldNames);
		if(verbose){
			String s = 	entryId + " \t| " +
						dbId + " \t| " + 
						tableId; 
						
			for(DBValue value: values){
				s += " \t| " + value;
			}
			System.out.println("Entry: " + s);
		}
		return true;
	}
	
	public boolean deleteEntry(int id){
		Entry entryToDelete = entries.get(id);
		if(entryToDelete == null)return false;
		for(Group g : groups.values()){
			if(entryToDelete.isReferredBy(g)){
				return false;
			}
		}
		entryIndexPKey.remove(entryToDelete.getGroupValue(primaryKey));
		entries.remove(id);
		return true;		
	}

	public Group addGroup(String groupName, Field[] fields, boolean unique,
			Group refersTo) throws DBValueException, InvalidFieldException {
		Group g = addGroup(groupName, fields, unique);
		if(g != null)g.referTo(refersTo);
		return g;
	}

	public boolean isReferredExternally() {
		for(Group g: groups.values()){
			if(g.isReferredByOtherTable())return true;
		}
		return false;
	}

	public void cleanUp() {
		for(Group g: groups.values() ){
			g.cleanUp();
		}
		
	}

	
}
